class KeyValue(object):
  """The basic element of each rule is a key-pair value."""
  
  def __init__(self, key, value):
    self.key = key
    self.value = value
  
  
  def __str__(self):
    return "%s = %s" % (self.key, self.value)
  
  
  def apply(self, sound):
    """Determine whether a given sound (a set of key-value pairs) contains
    this key-value pair."""
    
    for key, value in sound.pairs.iteritems():
      if key == self.key and value == self.value:
        return True
    return False


class _operator(object):
  # Helper class to create the operators of key-value expression
  
  def __init__(self, name, unary):
    self.name = name
    self.unary = unary


class KeyValueExpressionOperators(object):
  """This class is only used to store the possible operations performed on
  KeyValueExpressions. 
  """
  
  # Constant operators used in the expressions 
  NONE = _operator("none", True)
  NOT = _operator("not", True)
  AND = _operator("and", False)
  OR = _operator("or", False)


class KeyValueExpression(object):
  """This class is used to instantiate expressions made up of classes (with
  keywords "and", "or" and "not" and parenthesis to change order of execution).
  """
  
  # The class uses a tree to represent the expression. As such, it has two
  # fields:
  #  - operation: KeyValueExpressionOperators.NONE, .AND, .OR or .NOT;
  #  - pointers: a list of KeyValueExpression or KeyValue objects.
  
  def __init__(self, operator, pointers=None):
    """This method initializes the expression. There are two supported
    signatures: with one argument, it is assumed it is a KeyValue abject that
    stands as the full expression; with two arguments, the first is the
    operator to apply to the sub-expressions (AND, OR or NOT) and the second
    is a list of KeyNodeExpression objects.
    """
    
    # Do a little overloading emulation
    if pointers is None:
      operator = KeyValueExpressionOperators.NONE
      pointers = [operator]
    
    # Validate input
    if operator.unary and len(pointers) != 1:
      raise TypeError("Operator %s is unary" % operator.name)
    
    # Store the values
    self.operator = operator
    self.pointers = pointers
  
  
  def __str__(self):
    if self is KeyValueExpression.default_expression:
      return "anything"
    
    if self.operator == KeyValueExpressionOperators.NONE:
      return str(self.pointers[0])
    
    if self.operator == KeyValueExpressionOperators.NOT:
      return "not %s" % self.pointers[0]
    
    glue = " %s " % self.operator.name
    
    return "(%s)" % glue.join(str(i) for i in self.pointers)
  
  
  def apply(self, sound):
    """Determines whether a given sound satisfies this expression."""
    
    if self.operation == KeyValueExpressionOperators.NONE:
      return self.pointers[0].apply(sound)
    
    elif self.operation == KeyValueExpressionOperators.NOT:
      return not self.pointers[0].apply(sound)
    
    elif self.operation == KeyValueExpressionOperators.OR:
      for key_value in self.pointers:
        if key_value.apply(sound):
          return True
      return False
    
    elif self.operation == KeyValueExpressionOperators.AND:
      for key_value in self.pointers:
        if not key_value.apply(sound):
          return False
      return True
  
  
  @classmethod
  def default_expression(cls):
    return cls(KeyValueExpressionOperators.AND, [])


KeyValueExpression.default_expression = KeyValueExpression.default_expression()


class Rule(object):
  """Each rule determines which key-value pairs are allowed for a sound, based
  on which other key-value pairs have been assigned to it. A rule has a premise,
  an action and an object. These three specify, respectively:
   - The key-value pair (or pair expression) that triggers the rule;
   - An "allow" or "disallow" action, that determines whether the next pair is
     allowed or not;
   - The pair that can (or cannot) be applied to the sound
  The premise can be empty, in which case the rule is applied to all sounds, and
  either a pair or a group of pairs can be specified for the object. In case of
  groups, this "grouped" rule stands for all the rules that could be constructed
  by taking a pair from the group separately.
  """
  
  # Store the allowed actions
  ALLOW = -1
  DISALLOW = -2
  
  
  def __init__(self, premise, action, object):
    """Initiate the rule with a key-value expression, and action and a key-value
    pair (or group of key-value pairs)
    """
    
    self.premise = premise
    self.action = action
    self.object = object
  
  
  def __str__(self):
    premise = str(self.premise)
    if self.action == Rule.ALLOW:
      action = "allow"
    elif self.action == Rule.DISALLOW:
      action = "disallow"
    else:
      action = self.action
    
    if type(self.object) == ModelGroup:
      object = "group %s" % self.object.name
    elif type(self.object) == list:
      object = ", ".join(str(i) for i in self.object)
    
    return "If %s, then %s %s" % (premise, action, object)


class ModelGroup(object):
  """Holds the key-value pairs of a named group."""
  
  def __init__(self, name, pairs):
    """This initializes the group with a name and a list of key-value pairs."""
    
    self.name = name
    self.pairs = pairs
  
  
  def __str__(self):
    return "Group %s: %s" % (self.name, ", ".join(str(i) for i in self.pairs))


class PhonologyModel(object):
  """The model of a phonology is a set of rules that describe which key-value
  pairs a sound can have. These rules are applied to a sound with several
  key-value pairs to determine which further pairs are allowed or disallowed.
  """
  
  def __init__(self):
    self.groups = []
    self.rules = []
  
  
  @classmethod
  def read_from_file(cls, filename):
    """This method reads a file describing an Aforlang Phonology Model in XML
    and recreates it in a format more machine friendly 
    """
    
    from model_parser import parse_xml_model
    generator = parse_xml_model(filename)
    
    result = PhonologyModel()
    
    for obj in generator:
      # Do stuff here with the object being received
      if type(obj) == Rule:
        result.rules.append(obj)
      elif type(obj) == ModelGroup:
        result.groups.append(obj)
        
    return result











